<?php

/*

Desktopd Picture Server
Copyright (C) 2015  Desktopd Developer(s)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

https://www.gnu.org/licenses/agpl.html

*/


namespace Desktopd\PictureServer;

use Desktopd\PlatformTools\Log;


class Downloader extends \Desktopd\Desktopd\Downloader implements \IteratorAggregate, \Countable {
    protected $uriList = array();
    protected $inProgress = false;
    
    protected $statusListeners = array();
    protected $curlMultiErrors = array();
    protected $curlMultiDoneURIs = array();
    
    
    public function addURI ($uri) {
        if ($this->inProgress) {
            return false;
        }
        
        if (!$this->checkURI($uri)) {
            return false;
        }
        
        $uri = $this->encodeURI($uri);
        
        if (!in_array($uri, $this->uriList, true)) {
            $this->uriList[] = $uri;
        }
        
        $this->probeHost($uri);
        
        return count($this->uriList);
    }
    
    public function addStatusListener (/*callable*/ $callback) {
        if (!is_callable($callback)) {
            return false;
        }
        
        if (in_array($callback, $this->statusListeners, true)) {
            return true;
        }
        
        $this->statusListeners[] = $callback;
        return true;
    }
    
    protected function callStatusListeners ($uri, $success = true, $message = 'Done') {
        foreach ($this->statusListeners as $callback) {
            $callback("$uri", (bool) $success, "$message");
        }
    }
    
    protected function formatBinaryPrefix ($number, $suffix = '') {
        switch (true) {
            case ($number < 1024 * 10):
                $formatted = sprintf('%d', round($number));
                break;
            
            case ($number < 1024 * 1024 * 10):
                $formatted = sprintf('%dKi', round($number / 1024));
                break;
            
            case ($number < 1024 * 1024 * 1024 * 10):
                $formatted = sprintf('%dMi', round($number / 1024 / 1024));
                break;
            
            default:
                $formatted = sprintf('%dGi', round($number / 1024 / 1024 / 1024));
                break;
            
        }
        
        return "{$formatted}{$suffix}";
    }
    
    protected function curlMultiGetStatus ($multi) {
        while ($info = curl_multi_info_read($multi)) {
            $curl = isset($info['handle']) ? $info['handle'] : null;
            if (!is_resource($curl)) {
                continue;
            }
            
            $uri = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
            
            if ((!isset($info['msg'])) || CURLMSG_DONE !== $info['msg']) {
                $this->callStatusListeners($uri, false, 'Unknown status');
                continue;
            }
            
            if (!isset($info['result'])) {
                $this->callStatusListeners($uri, false, 'Unknown error');
                continue;
            }
            
            if (CURLE_OK === $info['result']) {
                $total = $this->count();
                $this->curlMultiDoneURIs[] = $uri;
                $done = $this->countDoneURIs();
                
                $totalTime = curl_getinfo($curl, CURLINFO_TOTAL_TIME);
                $size = curl_getinfo($curl, CURLINFO_SIZE_DOWNLOAD);
                $speed = curl_getinfo($curl, CURLINFO_SPEED_DOWNLOAD);
                
                $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
                
                $formattedSize = $this->formatBinaryPrefix($size, 'B');
                $formattedSpeed = $this->formatBinaryPrefix($speed, 'B/s');
                
                $this->callStatusListeners($uri, true, "Done ($done/$total) ($formattedSize in $totalTime s, $formattedSpeed) (Content-Type: $contentType)");
                continue;
            }
            
            if (function_exists('curl_strerror')) {
                $error = curl_strerror($info['result']);
            } else {
                $error = 'ERROR';
            }
            $this->callStatusListeners($uri, false, $error);
            
            $this->curlMultiErrors[(int) $curl] = $error;
        }
        
        if (function_exists('pcntl_signal_dispatch')) {
            pcntl_signal_dispatch();
        }
    }
    
    public function getDoneURIs () {
        return $this->curlMultiDoneURIs;
    }
    
    public function countDoneURIs () {
        return count($this->curlMultiDoneURIs);
    }
    
    public function fetch (PictureDirectory $destination) {
        if (count($this->uriList) < 1) {
            // empty list given
            return true;
        }
        
        $this->inProgress = true;
        $share = $this->share;
        
        $curlHandles = array();
        foreach ($this->uriList as $uri) {
            $curl = curl_init($uri);
            
            curl_setopt($curl, CURLOPT_AUTOREFERER, true);
            curl_setopt($curl, CURLOPT_BINARYTRANSFER, true);
            curl_setopt($curl, CURLOPT_VERBOSE, true);
            if (defined('CURLOPT_CERTINFO')) {
                curl_setopt($curl, CURLOPT_CERTINFO, true);
            }
            curl_setopt($curl, CURLOPT_DNS_USE_GLOBAL_CACHE, false);
            curl_setopt($curl, CURLOPT_FAILONERROR, true);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
            curl_setopt($curl, CURLINFO_HEADER_OUT, true); // request header
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
            curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
            curl_setopt($curl, CURLOPT_ENCODING, "gzip, deflate");
            //curl_setopt($curl, CURLOPT_INTERFACE, '192.168.1.10');
            curl_setopt($curl, CURLOPT_SSL_CIPHER_LIST, $this->ciphers);
            curl_setopt($curl, CURLOPT_USERAGENT, $this->userAgent);
            
            $dataStream = $destination->createDataStream($uri);
            if (false === $dataStream) {
                continue;
            }
            curl_setopt($curl, CURLOPT_FILE, $dataStream);
            
            $headerStream = $destination->createSubStream($uri, 'header');
            if (false !== $headerStream) {
                curl_setopt($curl, CURLOPT_WRITEHEADER, $headerStream);
            }
            
            $errorStream = $destination->createSubStream($uri, 'stderr');
            if (false !== $errorStream) {
                curl_setopt($curl, CURLOPT_STDERR, $errorStream);
            }
            
            if (is_resource($share)) {
                curl_setopt($curl, CURLOPT_SHARE, $share);
            }
            
            $curlHandles[] = $curl;
        }
        
        unset($dataStream, $headerStream, $errorStream, $curl);
        
        
        // download
        $multi = curl_multi_init();
        foreach ($curlHandles as $curl) {
            if (!is_resource($curl)) continue;
            curl_multi_add_handle($multi, $curl);
        }
        
        $this->curlMultiDoneURIs = array();
        $active = null;
        do {
            $mrc = curl_multi_exec($multi, $active);
            $this->curlMultiGetStatus($multi);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        
        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($multi) == -1) {
                usleep(100);
            }
            do {
                $mrc = curl_multi_exec($multi, $active);
                $this->curlMultiGetStatus($multi);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
        
        
        // cleanup
        $failed = array();
        foreach ($curlHandles as $curl) {
            if (!is_resource($curl)) continue;
            
            $info = curl_getinfo($curl);
            $infoStream = $destination->createSubStream($info['url'], 'info');
            if (false !== $infoStream) {
                fwrite($infoStream, print_r($info, true));
                $destination->closeStream($infoStream);
            }
            unset($infoStream);
            
            try {
                if (curl_errno($curl)) {
                    throw new \RuntimeException(curl_error($curl));
                }
                
                if (isset($this->curlMultiErrors[(int) $curl])) {
                    throw new \RuntimeException($this->curlMultiErrors[(int) $curl]);
                }
            } catch (\Exception $e) {
                // error
                $error = $e->getMessage();
                $errorStream = $destination->createSubStream($info['url'], 'error');
                if (false !== $errorStream) {
                    fwrite($errorStream, print_r($info, true));
                    $destination->closeStream($errorStream);
                }
                unset($errorStream);
                
                // delete data
                $destination->deleteData($info['url']);
                
                $failed[] = $info['url'];
            }
        }
        
        
        foreach ($curlHandles as $curl) {
            if (!is_resource($curl)) {
                continue;
            }
            
            if (is_resource($multi)) {
                curl_multi_remove_handle($multi, $curl);
            }
            curl_close($curl);
        }
        
        if (is_resource($multi)) {
            curl_multi_close($multi);
        }
        
        $this->uriList = array();
        $this->inProgress = false;
        return $failed;
    }
    
    public function getIterator () {
        return new \ArrayIterator($this->uriList);
    }
    
    public function count () {
        return count($this->uriList);
    }
    
}


// vim: ts=4 et ai

